package com.aizuda.encrypt.interceptor;

import cn.hutool.cache.Cache;
import cn.hutool.cache.CacheUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.aizuda.encrypt.config.EncryptProperties;
import com.aizuda.encrypt.constant.EncryptConstant;
import com.aizuda.encrypt.entity.EncryptInfo;
import com.aizuda.encrypt.entity.EncryptRule;
import com.aizuda.encrypt.entity.EncryptedValuesParams;
import com.aizuda.encrypt.handler.JSQLStatementContext;
import com.aizuda.encrypt.jsqlparse.entity.JSQLCryptExpression;
import com.aizuda.encrypt.util.ParseSQLUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

import java.sql.Connection;
import java.util.List;
import java.util.Map;
import java.util.Properties;

/**
 * Mybatis拦截器 - 加密数据查询条件拦截
 *
 * @author nn200433
 * @date 2024-03-25 02:31:59
 */
@Slf4j
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class EncryptQueryInterceptor implements Interceptor {

    private static final Cache<String, String> lruCache = CacheUtil.newLRUCache(50);
    private final        EncryptProperties     cfg;
    private final        EncryptRule           rule;

    public EncryptQueryInterceptor(EncryptProperties properties) {
        cfg  = properties;
        rule = new EncryptRule(properties);
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            final Object          realTarget     = ParseSQLUtil.getRealTarget(invocation.getTarget());
            final MetaObject      metaObject     = SystemMetaObject.forObject(realTarget);
            final MappedStatement ms             = (MappedStatement) metaObject.getValue(EncryptConstant.MAPPED_STATEMENT_KEY);
            final SqlCommandType  sqlCommandType = ms.getSqlCommandType();
            if (sqlCommandType != SqlCommandType.SELECT) {
                // 不是 select 语句时返回
                return invocation.proceed();
            }
            final Object                 value             = metaObject.getValue(EncryptConstant.SQL_KEY);
            final String                 sql               = Convert.toStr(value);
            final Object                 parameterObject   = metaObject.getValue(EncryptConstant.PARAMETER_OBJECT_KEY);
            final List<ParameterMapping> parameterMappings = (List<ParameterMapping>) metaObject.getValue(EncryptConstant.PARAMETER_MAPPINGS_KEY);
            // 解析where中的表达式
            final JSQLStatementContext      context        = new JSQLStatementContext(sql, rule);
            final List<EncryptInfo>         encryptInfos   = context.createEncryptInfos();
            final List<JSQLCryptExpression> expressionList = context.getExpressionList();
            log.debug("---> [Mybatis 查询拦截器] query 待处理 sql = {}", sql);
            // 查询条件处理
            encryptWrapperHandle(encryptInfos, metaObject, sql, parameterObject, parameterMappings);
            // 特殊查询字符处理
            final Boolean isLikeExpressionEscape = cfg.getIsLikeExpressionEscape();
            if (isLikeExpressionEscape) {
                specialConditionsHandle(expressionList, metaObject, sql, parameterObject, parameterMappings);
            }
        } catch (Exception e) {
            log.error("---> [Mybatis 查询拦截器] 拦截出现异常...，原因：{}", e.getMessage(), e);
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }

    /**
     * 特殊情况处理
     *
     * @param expressionList    表达式列表
     * @param metaObject        元对象
     * @param sql               sql
     * @param parameterObject   参数对象
     * @param parameterMappings 参数映射
     * @author nn200433
     */
    private void specialConditionsHandle(List<JSQLCryptExpression> expressionList, MetaObject metaObject, String sql,
                                         Object parameterObject, List<ParameterMapping> parameterMappings) {
        for (JSQLCryptExpression expression : expressionList) {
            final String eachSql = expression.getSql();
            if (null != parameterMappings && null != parameterObject) {
                Object           value            = null;
                ParameterMapping parameterMapping = parameterMappings.get(expression.getIndex() - 1);
                String           property         = ParseSQLUtil.getNewProperty(parameterMapping.getProperty());
                if (parameterMappings.size() == 1) {
                    value = parameterObject;
                }
                if (value instanceof Map || parameterMappings.size() > 1) {
                    try {
                        value = metaObject.getValue(property);
                    } catch (Exception e) {
                        log.error("---> metaObject.getValue(\"{}\") 出现错误。错误原因：{}", property, e.getMessage());
                        continue;
                    }
                }
                final String originalValues = Convert.toStr(value);
                if (StrUtil.containsIgnoreCase(eachSql, "like") && StrUtil.containsAny(originalValues, "\\\\", "_", "%", "'")) {
                    String       resultOriginalValues = StrUtil.blankToDefault(lruCache.get(originalValues), originalValues);
                    final String columnName           = expression.getColumnName();
                    final String newValue = ParseSQLUtil.getNewValue(sql, resultOriginalValues, (sql_, str) -> {
                        /*
                        druid 输出 ：select count(*) as total from sys_dict_group where del_flag = '0' and code like '%''%'
                        mysql 实际执行：2023-12-04T09:30:52.637340Z	6371480 Query	SELECT COUNT(*) AS total FROM sys_dict_group WHERE del_flag = '0' AND (code LIKE '%\'%')
                        总结，垃圾 druid
                         */
                        // 特殊字符转义
                        str = str.replaceAll("\\\\", "\\\\\\\\");
                        str = str.replaceAll("_", "\\\\_");
                        str = str.replaceAll("'", "\'");
                        str = str.replaceAll("%", "\\\\%");
                        // str = replaceAll(str, "\\\\", "\\");
                        // str = replaceAll(str, "_", "\\_");
                        // str = replaceAll(str, "'", "\\'");
                        // str = replaceAll(str, "%", "\\%");
                        return str;
                    });
                    log.debug("---> [Mybatis SQL 条件拦截处理] 字段 = {} ， 处理前 = {} ， 处理后 = {}", columnName, originalValues, newValue);
                    lruCache.put(newValue, resultOriginalValues);
                    metaObject.setValue(property, newValue);
                }
            }
        }
    }

    /**
     * 处理where条件
     *
     * @param encryptInfos      加密条件
     * @param metaObject        元对象
     * @param sql               sql字符串
     * @param parameterObject   参数对象
     * @param parameterMappings 参数映射
     * @author nn200433
     */
    private void encryptWrapperHandle(List<EncryptInfo> encryptInfos, MetaObject metaObject, String sql,
                                      Object parameterObject, List<ParameterMapping> parameterMappings) {
        if (CollectionUtil.isEmpty(encryptInfos)) {
            return;
        }
        for (EncryptInfo encryptInfo : encryptInfos) {
            final int index = encryptInfo.getIndex();
            if (parameterMappings != null && parameterObject != null) {
                final ParameterMapping parameterMapping = parameterMappings.get(index - 1);
                final String           propertyStr      = parameterMapping.getProperty();
                final String           property         = ParseSQLUtil.getNewProperty(propertyStr);
                // 假设该数据已经加密，查询缓存中是否存在加密数据的原值
                /* count查询修改加密字段数据后，带入数据查询时会出现问题
                举个例子：
                count(*) ---> [Mybatis SQL 加密拦截] 表名 = sys_user ， 字段 = user_name ， 加密前 = sp10 ， 加密后 = 6d48ac1a94008ed6728cf46a2d6219c4
                data ---> [Mybatis SQL 加密拦截] 表名 = sys_user ， 字段 = user_name ， 加密前 = 6d48ac1a94008ed6728cf46a2d6219c4 ， 加密后 = 8e504242d68484075fd532253dc6fff548ff17eda264fe298a4fb9fd10e41f7aa0db571271363bb335768b3e605f54c2
                */
                final String                originalValues       = Convert.toStr(metaObject.getValue(property));
                String                      lastOriginalValues   = lruCache.get(originalValues);
                String                      resultOriginalValues = StrUtil.blankToDefault(lastOriginalValues, originalValues);
                final String                tableName            = encryptInfo.getTableName();
                final String                columnName           = encryptInfo.getColumnName();
                EncryptedValuesParams       params               = new EncryptedValuesParams();
                // 不加前缀
                params.setIsAddIndex(Boolean.FALSE);
                params.setSqlType(SqlCommandType.SELECT);
                final String newValue = ParseSQLUtil.getNewValue(sql, resultOriginalValues, (sql_, s) -> {
                    String ev = Convert.toStr(rule.getEncryptedValues(tableName, columnName, s, params));
                    log.debug("---> [Mybatis SQL 加密拦截] 表名 = {} ， 字段 = {} ， 加密前 = {} ， 加密后 = {}", tableName, columnName, s, ev);
                    return ev;
                });
                lruCache.put(newValue, resultOriginalValues);
                metaObject.setValue(property, newValue);
            }
        }
    }

}
